/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.startup;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.catalina.Globals;
import org.apache.catalina.security.SecurityClassLoad;
import org.apache.catalina.startup.ClassLoaderFactory.Repository;
import org.apache.catalina.startup.ClassLoaderFactory.RepositoryType;
import org.apache.jasper.servlet.JasperInitializer;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;


/**
 * Bootstrap loader for Catalina. This application constructs a class loader
 * for use in loading the Catalina internal classes (by accumulating all of the
 * JAR files found in the "server" directory under "catalina.home"), and
 * starts the regular execution of the container. The purpose of this
 * roundabout approach is to keep the Catalina internal classes (and any
 * other classes they depend on, such as an XML parser) out of the system
 * class path and therefore not visible to application level classes.
 *
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 */
public final class Bootstrap {

  {
    JasperInitializer initializer = new JasperInitializer();
  }
  private static final Log log = LogFactory.getLog(Bootstrap.class);

  /**
   * Daemon object used by main.
   */
  private static Bootstrap daemon = null;

  private static final File catalinaBaseFile;
  private static final File catalinaHomeFile;

  private static final Pattern PATH_PATTERN = Pattern.compile("(\".*?\")|(([^,])*)");

  static {
    // Will always be non-null
    String userDir = System.getProperty("user.dir");

    // Home first
    String home = System.getProperty(Globals.CATALINA_HOME_PROP);
    File homeFile = null;

    if (home != null) {
      File f = new File(home);
      try {
        homeFile = f.getCanonicalFile();
      } catch (IOException ioe) {
        homeFile = f.getAbsoluteFile();
      }
    }

    if (homeFile == null) {
      // First fall-back. See if current directory is a bin directory
      // in a normal Tomcat install
      File bootstrapJar = new File(userDir, "bootstrap.jar");

      if (bootstrapJar.exists()) {
        File f = new File(userDir, "..");
        try {
          homeFile = f.getCanonicalFile();
        } catch (IOException ioe) {
          homeFile = f.getAbsoluteFile();
        }
      }
    }

    if (homeFile == null) {
      // Second fall-back. Use current directory
      File f = new File(userDir);
      try {
        homeFile = f.getCanonicalFile();
      } catch (IOException ioe) {
        homeFile = f.getAbsoluteFile();
      }
    }

    catalinaHomeFile = homeFile;
    System.setProperty(Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());

    // Then base
    String base = System.getProperty(Globals.CATALINA_BASE_PROP);
    if (base == null) {
      catalinaBaseFile = catalinaHomeFile;
    } else {
      File baseFile = new File(base);
      try {
        baseFile = baseFile.getCanonicalFile();
      } catch (IOException ioe) {
        baseFile = baseFile.getAbsoluteFile();
      }
      catalinaBaseFile = baseFile;
    }
    System.setProperty(Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
  }

  // -------------------------------------------------------------- Variables


  /**
   * Daemon reference.
   */
  private Object catalinaDaemon = null;


  ClassLoader commonLoader = null;
  ClassLoader catalinaLoader = null;
  ClassLoader sharedLoader = null;


  // -------------------------------------------------------- Private Methods


  private void initClassLoaders() {
    try {
      commonLoader = createClassLoader("common", null);
      if (commonLoader == null) {
        // no config file, default to this loader - we might be in a 'single' env.
        commonLoader = this.getClass().getClassLoader();
      }
      catalinaLoader = createClassLoader("server", commonLoader);
      sharedLoader = createClassLoader("shared", commonLoader);
    } catch (Throwable t) {
      handleThrowable(t);
      log.error("Class loader creation threw exception", t);
      System.exit(1);
    }
  }


  private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {

    String value = CatalinaProperties.getProperty(name + ".loader");
    if ((value == null) || (value.equals("")))
      return parent;

    value = replace(value);

    List<Repository> repositories = new ArrayList<>();

    String[] repositoryPaths = getPaths(value);

    for (String repository : repositoryPaths) {
      // Check for a JAR URL repository
      try {
        @SuppressWarnings("unused")
        URL url = new URL(repository);
        repositories.add(new Repository(repository, RepositoryType.URL));
        continue;
      } catch (MalformedURLException e) {
        // Ignore
      }

      // Local repository
      if (repository.endsWith("*.jar")) {
        repository = repository.substring(0, repository.length() - "*.jar".length());
        repositories.add(new Repository(repository, RepositoryType.GLOB));
      } else if (repository.endsWith(".jar")) {
        repositories.add(new Repository(repository, RepositoryType.JAR));
      } else {
        repositories.add(new Repository(repository, RepositoryType.DIR));
      }
    }

    return ClassLoaderFactory.createClassLoader(repositories, parent);
  }


  /**
   * System property replacement in the given string.
   *
   * @param str The original string
   * @return the modified string
   */
  protected String replace(String str) {
    // Implementation is copied from ClassLoaderLogManager.replace(),
    // but added special processing for catalina.home and catalina.base.
    String result = str;
    int pos_start = str.indexOf("${");
    if (pos_start >= 0) {
      StringBuilder builder = new StringBuilder();
      int pos_end = -1;
      while (pos_start >= 0) {
        builder.append(str, pos_end + 1, pos_start);
        pos_end = str.indexOf('}', pos_start + 2);
        if (pos_end < 0) {
          pos_end = pos_start - 1;
          break;
        }
        String propName = str.substring(pos_start + 2, pos_end);
        String replacement;
        if (propName.length() == 0) {
          replacement = null;
        } else if (Globals.CATALINA_HOME_PROP.equals(propName)) {
          replacement = getCatalinaHome();
        } else if (Globals.CATALINA_BASE_PROP.equals(propName)) {
          replacement = getCatalinaBase();
        } else {
          replacement = System.getProperty(propName);
        }
        if (replacement != null) {
          builder.append(replacement);
        } else {
          builder.append(str, pos_start, pos_end + 1);
        }
        pos_start = str.indexOf("${", pos_end + 1);
      }
      builder.append(str, pos_end + 1, str.length());
      result = builder.toString();
    }
    return result;
  }


  /**
   * Initialize daemon.
   * 
   * @throws Exception Fatal initialization error
   */
  public void init() throws Exception {

    initClassLoaders();

    Thread.currentThread().setContextClassLoader(catalinaLoader);

    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // Load our startup class and call its process() method
    if (log.isDebugEnabled())
      log.debug("Loading startup class");
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // Set the shared extensions class loader
    if (log.isDebugEnabled())
      log.debug("Setting startup class properties");
    String methodName = "setParentClassLoader";
    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Class.forName("java.lang.ClassLoader");
    Object paramValues[] = new Object[1];
    paramValues[0] = sharedLoader;
    Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
    method.invoke(startupInstance, paramValues);

    catalinaDaemon = startupInstance;

  }


  /**
   * Load daemon.
   */
  private void load(String[] arguments) throws Exception {

    // Call the load() method
    String methodName = "load";
    Object param[];
    Class<?> paramTypes[];
    if (arguments == null || arguments.length == 0) {
      paramTypes = null;
      param = null;
    } else {
      paramTypes = new Class[1];
      paramTypes[0] = arguments.getClass();
      param = new Object[1];
      param[0] = arguments;
    }
    Method method = catalinaDaemon.getClass().getMethod(methodName, paramTypes);
    if (log.isDebugEnabled())
      log.debug("Calling startup class " + method);
    method.invoke(catalinaDaemon, param);

  }


  /**
   * getServer() for configtest
   */
  private Object getServer() throws Exception {

    String methodName = "getServer";
    Method method = catalinaDaemon.getClass().getMethod(methodName);
    return method.invoke(catalinaDaemon);

  }


  // ----------------------------------------------------------- Main Program


  /**
   * Load the Catalina daemon.
   * 
   * @param arguments Initialization arguments
   * @throws Exception Fatal initialization error
   */
  public void init(String[] arguments) throws Exception {

    init();
    load(arguments);

  }


  /**
   * Start the Catalina daemon.
   * 
   * @throws Exception Fatal start error
   */
  public void start() throws Exception {
    if (catalinaDaemon == null)
      init();

    Method method = catalinaDaemon.getClass().getMethod("start", (Class[]) null);
    method.invoke(catalinaDaemon, (Object[]) null);

  }


  /**
   * Stop the Catalina Daemon.
   * 
   * @throws Exception Fatal stop error
   */
  public void stop() throws Exception {

    Method method = catalinaDaemon.getClass().getMethod("stop", (Class[]) null);
    method.invoke(catalinaDaemon, (Object[]) null);

  }


  /**
   * Stop the standalone server.
   * 
   * @throws Exception Fatal stop error
   */
  public void stopServer() throws Exception {

    Method method = catalinaDaemon.getClass().getMethod("stopServer", (Class[]) null);
    method.invoke(catalinaDaemon, (Object[]) null);

  }


  /**
   * Stop the standalone server.
   * 
   * @param arguments Command line arguments
   * @throws Exception Fatal stop error
   */
  public void stopServer(String[] arguments) throws Exception {

    Object param[];
    Class<?> paramTypes[];
    if (arguments == null || arguments.length == 0) {
      paramTypes = null;
      param = null;
    } else {
      paramTypes = new Class[1];
      paramTypes[0] = arguments.getClass();
      param = new Object[1];
      param[0] = arguments;
    }
    Method method = catalinaDaemon.getClass().getMethod("stopServer", paramTypes);
    method.invoke(catalinaDaemon, param);

  }


  /**
   * Set flag.
   * 
   * @param await <code>true</code> if the daemon should block
   * @throws Exception Reflection error
   */
  public void setAwait(boolean await) throws Exception {

    Class<?> paramTypes[] = new Class[1];
    paramTypes[0] = Boolean.TYPE;
    Object paramValues[] = new Object[1];
    paramValues[0] = Boolean.valueOf(await);
    Method method = catalinaDaemon.getClass().getMethod("setAwait", paramTypes);
    method.invoke(catalinaDaemon, paramValues);

  }

  public boolean getAwait() throws Exception {
    Class<?> paramTypes[] = new Class[0];
    Object paramValues[] = new Object[0];
    Method method = catalinaDaemon.getClass().getMethod("getAwait", paramTypes);
    Boolean b = (Boolean) method.invoke(catalinaDaemon, paramValues);
    return b.booleanValue();
  }


  /**
   * Destroy the Catalina Daemon.
   */
  public void destroy() {

    // FIXME

  }


  /**
   * Main method and entry point when starting Tomcat via the provided
   * scripts.
   *
   * @param args Command line arguments to be processed
   */
  public static void main(String args[]) {

    if (daemon == null) {
      // Don't set daemon until init() has completed
      Bootstrap bootstrap = new Bootstrap();
      try {
        bootstrap.init();
      } catch (Throwable t) {
        handleThrowable(t);
        t.printStackTrace();
        return;
      }
      daemon = bootstrap;
    } else {
      // When running as a service the call to stop will be on a new
      // thread so make sure the correct class loader is used to prevent
      // a range of class not found exceptions.
      Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
    }

    try {
      String command = "start";
      if (args.length > 0) {
        command = args[args.length - 1];
      }

      if (command.equals("startd")) {
        args[args.length - 1] = "start";
        daemon.load(args);
        daemon.start();
      } else if (command.equals("stopd")) {
        args[args.length - 1] = "stop";
        daemon.stop();
      } else if (command.equals("start")) {
        daemon.setAwait(true);
        daemon.load(args);
        daemon.start();
        if (null == daemon.getServer()) {
          System.exit(1);
        }
      } else if (command.equals("stop")) {
        daemon.stopServer(args);
      } else if (command.equals("configtest")) {
        daemon.load(args);
        if (null == daemon.getServer()) {
          System.exit(1);
        }
        System.exit(0);
      } else {
        log.warn("Bootstrap: command \"" + command + "\" does not exist.");
      }
    } catch (Throwable t) {
      // Unwrap the Exception for clearer error reporting
      if (t instanceof InvocationTargetException && t.getCause() != null) {
        t = t.getCause();
      }
      handleThrowable(t);
      t.printStackTrace();
      System.exit(1);
    }

  }


  /**
   * Obtain the name of configured home (binary) directory. Note that home and
   * base may be the same (and are by default).
   * 
   * @return the catalina home
   */
  public static String getCatalinaHome() {
    return catalinaHomeFile.getPath();
  }


  /**
   * Obtain the name of the configured base (instance) directory. Note that
   * home and base may be the same (and are by default). If this is not set
   * the value returned by {@link #getCatalinaHome()} will be used.
   * 
   * @return the catalina base
   */
  public static String getCatalinaBase() {
    return catalinaBaseFile.getPath();
  }


  /**
   * Obtain the configured home (binary) directory. Note that home and
   * base may be the same (and are by default).
   * 
   * @return the catalina home as a file
   */
  public static File getCatalinaHomeFile() {
    return catalinaHomeFile;
  }


  /**
   * Obtain the configured base (instance) directory. Note that
   * home and base may be the same (and are by default). If this is not set
   * the value returned by {@link #getCatalinaHomeFile()} will be used.
   * 
   * @return the catalina base as a file
   */
  public static File getCatalinaBaseFile() {
    return catalinaBaseFile;
  }


  // Copied from ExceptionUtils since that class is not visible during start
  private static void handleThrowable(Throwable t) {
    if (t instanceof ThreadDeath) {
      throw (ThreadDeath) t;
    }
    if (t instanceof VirtualMachineError) {
      throw (VirtualMachineError) t;
    }
    // All other instances of Throwable will be silently swallowed
  }


  // Protected for unit testing
  protected static String[] getPaths(String value) {

    List<String> result = new ArrayList<>();
    Matcher matcher = PATH_PATTERN.matcher(value);

    while (matcher.find()) {
      String path = value.substring(matcher.start(), matcher.end());

      path = path.trim();
      if (path.length() == 0) {
        continue;
      }

      char first = path.charAt(0);
      char last = path.charAt(path.length() - 1);

      if (first == '"' && last == '"' && path.length() > 1) {
        path = path.substring(1, path.length() - 1);
        path = path.trim();
        if (path.length() == 0) {
          continue;
        }
      } else if (path.contains("\"")) {
        // Unbalanced quotes
        // Too early to use standard i18n support. The class path hasn't
        // been configured.
        throw new IllegalArgumentException(
            "The double quote [\"] character only be used to quote paths. It must "
                + "not appear in a path. This loader path is not valid: [" + value + "]");
      } else {
        // Not quoted - NO-OP
      }

      result.add(path);
    }
    return result.toArray(new String[result.size()]);
  }
}
